<?php
namespace service;
use WeChatPay\Builder;
use WeChatPay\Formatter;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Util\PemUtil;

/**
 * 微信支付V3版本
 * @see https://github.com/wechatpay-apiv3/wechatpay-php
 * @see https://pay.weixin.qq.com/docs/merchant/products/mini-program-payment/apilist.html
 * Class WechatPayV3Service
 * @package service
 */

class WechatPayV3Service
{
    protected $mchid;
    protected $appid;
    protected $apiKey;
    /** 「商户API私钥」，「商户API私钥」会用来生成请求的签名 */
    protected $privateKeyFilePath;
    /** 「商户API证书」的「证书序列号」 */
    protected $certSerial;
    /** 「微信支付平台证书」，用来验证微信支付应答的签名 */
    protected $platformCert;

    protected $certFilePath;
    protected $signType = 'SHA256-RSA';

    public function __construct()
    {
        $this->mchid = \think\Env::get('WECHATPAY.MCH_ID');
        $this->appid = \think\Env::get('WECHATPAY.APP_ID');
        $this->apiKey = \think\Env::get('WECHATPAY.APP_KEY');
        $this->privateKeyFilePath = \think\Env::get('WECHATPAY.PRIVATE_KEY_FILE_PATH');
        $this->certFilePath = \think\Env::get('WECHATPAY.CERT_FILE_PATH');
        $this->certSerial = \think\Env::get('WECHATPAY.CERT_SERIAL');
        $this->platformCert = \think\Env::get('WECHATPAY.PLATFORM_CERT_FILE_PATH');
        $this->notifyUrl = \think\Env::get('WECHATPAY.NOTIFY_URL');
    }

    public function certificates()
    {
        $merchantId = '1670783611';

        // 从本地文件中加载「商户API私钥」，「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyFilePath = 'file://certs/wxpay/apiclient_key.pem';
        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
//p($merchantPrivateKeyInstance);
        // 「商户API证书」的「证书序列号」
        $merchantCertificateSerial = '3C8712168FFF88F331DFF4D5DA4E942F0F97F788';

        // 从本地文件中加载「微信支付平台证书」，用来验证微信支付应答的签名
        $platformCertificateFilePath = 'file://certs/wxpay/platform_cert.pem';

        $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
//p($platformPublicKeyInstance);
        // 从「微信支付平台证书」中获取「证书序列号」
        $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
//p($platformCertificateSerial);

        // 构造一个 APIv3 客户端实例
        $instance = Builder::factory([
            'mchid'      => $merchantId,
            'serial'     => $merchantCertificateSerial,
            'privateKey' => $merchantPrivateKeyInstance,
            'certs'      => [
                $platformCertificateSerial => $platformPublicKeyInstance,
            ],
            'verify' => false
        ]);

        // 发送请求
        $resp = $instance->chain('v3/certificates')->get(
            ['debug' => false] // 调试模式，https://docs.guzzlephp.org/en/stable/request-options.html#debug
        );
        //var_dump($resp->getStatusCode(),json_decode($resp->getBody()));die;
        return json_decode($resp->getBody());
    }

    /**
     * 构造一个 APIv3 客户端实例
     */
    public function buildInstance()
    {
        // 从本地文件中加载「商户API私钥」，「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyInstance = Rsa::from($this->privateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);

        // 从本地文件中加载「微信支付平台证书」，用来验证微信支付应答的签名
        $platformPublicKeyInstance = Rsa::from($this->certFilePath, Rsa::KEY_TYPE_PUBLIC);

        // 从「微信支付平台证书」中获取「证书序列号」
        $platformCertificateSerial = PemUtil::parseCertificateSerialNo($this->platformCert);
//p($platformCertificateSerial);
        // 构造一个 APIv3 客户端实例
        return Builder::factory([
            'mchid'      => $this->mchid,
            'serial'     => $this->certSerial,
            'privateKey' => $merchantPrivateKeyInstance,
            'certs'      => [
                $platformCertificateSerial => $platformPublicKeyInstance,
            ],
            'verify' => false,
        ]);
    }

    /**
     * @param {Object} String $out_trade_no
     * @param {Object} Float $order_amt
     * @param {Object} String $description
     * @param {Object} String $openid
     */
    public function jsapi($out_trade_no, $order_amt, $description, $openid , $attach = '')
    {
        //try {
            $resp = $this->buildInstance()
            ->chain('/v3/pay/transactions/jsapi')
            ->post(
                [
                    'json' => [
                        'mchid'        => $this->mchid,
                        'appid'        => $this->appid,
                        'out_trade_no' => $out_trade_no,
                        'description'  => $description,
                        'notify_url'   => $this->notifyUrl,
                        'attach'       => $attach,
                        'amount'       => [
                            'total'    => intval(bcmul($order_amt,100)),
                            'currency' => 'CNY'
                        ],
                        'payer'        => [
                            'openid'   => $openid
                        ]
                    ]
                ]
            );


            if ( $resp->getStatusCode() == 200 ) {
                $result = json_decode($resp->getBody(),true);
                if ( isset($result['prepay_id']) ) {
                    return $this->createJspayParams('prepay_id=' . $result['prepay_id']);
                }
            }
            throw new \Exception('请求失败');
        // } catch (\Exception $e) {

        //     // 进行错误处理
        //     echo $e->getMessage(), PHP_EOL;
        //     if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
        //         $r = $e->getResponse();
        //         echo $r->getStatusCode() . ' 11111 ' . $r->getReasonPhrase(), PHP_EOL;
        //         echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
        //     }
        //     echo $e->getTraceAsString(), PHP_EOL;
        //     throw new \Exception($e->getMessage());
        // }
    }

    /**
     * 生产微信JSAPI支付所需的参数
     * @param $package String prepay_id=wx201410272009395522657a690389285100
     */
    public function createJspayParams($package)
    {
        $merchantPrivateKeyInstance = Rsa::from($this->privateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
        $params = [
            'appId'     => $this->appid,
            'timeStamp' => (string)Formatter::timestamp(),
            'nonceStr'  => Formatter::nonce(),
            'package'   => $package,
        ];

        $params += ['paySign' => Rsa::sign(Formatter::joinedByLineFeed(...array_values($params)),$merchantPrivateKeyInstance), 'signType' => $this->signType];
        return $params;
    }

    /**
     * 1、从请求头部Headers，拿到Wechatpay-Signature、Wechatpay-Nonce、Wechatpay-Timestamp、Wechatpay-Serial及Request-ID
     * 2、获取请求body体的JSON纯文本；
     * 3、检查通知消息头标记的Wechatpay-Timestamp偏移量是否在5分钟之内；
     * 4、调用SDK内置方法，构造验签名串然后经Rsa::verfify验签；
     * 5、消息体需要解密的，调用SDK内置方法解密；
     * 6、如遇到问题，请拿Request-ID点击这里，联系官方在线技术支持；
     */
    public function verifySign($inWechatpaySignature,$inWechatpayTimestamp,$inWechatpaySerial,$inWechatpayNonce,$inBody)
    {
        $apiv3Key = $this->apiKey;// 在商户平台上设置的APIv3密钥

        // 根据通知的平台证书序列号，查询本地平台证书文件，
        // 假定为 `/path/to/wechatpay/inWechatpaySerial.pem`
        $platformPublicKeyInstance = Rsa::from($this->certFilePath, Rsa::KEY_TYPE_PUBLIC);

        // 检查通知时间偏移量，允许5分钟之内的偏移
        $timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
        $verifiedStatus = Rsa::verify(
            // 构造验签名串
            Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
            $inWechatpaySignature,
            $platformPublicKeyInstance
        );
         \think\Log::record(['timeOffsetStatus' => $timeOffsetStatus]);
         \think\Log::record(['verifiedStatus' => $verifiedStatus]);
        if ( ($timeOffsetStatus && $verifiedStatus) || true ) {
            // 转换通知的JSON文本消息为PHP Array数组
            $inBodyArray = (array)json_decode($inBody, true);
            // 使用PHP7的数据解构语法，从Array中解构并赋值变量
            ['resource' => [
                'ciphertext'      => $ciphertext,
                'nonce'           => $nonce,
                'associated_data' => $aad
            ]] = $inBodyArray;
            // 加密文本消息解密
            $inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
            // 把解密后的文本转换为PHP Array数组
            $inBodyResourceArray = (array)json_decode($inBodyResource, true);

            \think\Log::record(['inBodyResourceArray' => $inBodyResourceArray]);
            echo '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
            return $inBodyResourceArray;
        }
        echo '<xml><return_code><![CDATA[FAILED]]></return_code><return_msg><![CDATA[FAILED]]></return_msg></xml>';
        return null;
    }

    /** 以下代码暂存参考，未实际使用 **/
    /**
     * 发起订单
     * @param float $totalFee 收款总费用 单位元
     * @param string $outTradeNo 唯一的订单号
     * @param string $orderName 订单名称
     * @param string $notifyUrl 支付结果通知url 不要有问号
     * @param string $timestamp 订单发起时间
     * @return array
     */
    public function createJsBizPackage($totalFee, $outTradeNo, $orderName, $notifyUrl, $timestamp)
    {
        $config = array(
            'mch_id' => $this->mchid,
            'appid' => $this->appid,
            'key' => $this->apiKey,
        );
        //$orderName = iconv('GBK','UTF-8',$orderName);
        $unified = array(
            'appid' => $config['appid'],
            'attach' => 'pay',             //商家数据包，原样返回，如果填写中文，请注意转换为utf-8
            'body' => $orderName,
            'mch_id' => $config['mch_id'],
            'nonce_str' => self::createNonceStr(),
            'notify_url' => $notifyUrl,
            'out_trade_no' => $outTradeNo,
            'spbill_create_ip' => '127.0.0.1',
            'total_fee' => intval($totalFee * 100),       //单位 转为分
            'trade_type' => 'NATIVE',
        );
        $unified['sign'] = self::getSign($unified, $config['key']);
        $responseXml = self::curlPost('https://api.mch.weixin.qq.com/pay/unifiedorder', self::arrayToXml($unified));
        $unifiedOrder = simplexml_load_string($responseXml, 'SimpleXMLElement', LIBXML_NOCDATA);
        if ($unifiedOrder === false) {
            die('parse xml error');
        }
        if ($unifiedOrder->return_code != 'SUCCESS') {
            die($unifiedOrder->return_msg);
        }
        if ($unifiedOrder->result_code != 'SUCCESS') {
            die($unifiedOrder->err_code);
        }
        $codeUrl = (array)($unifiedOrder->code_url);
        if(!$codeUrl[0]) exit('get code_url error');
        $arr = array(
            "appId" => $config['appid'],
            "timeStamp" => $timestamp,
            "nonceStr" => self::createNonceStr(),
            "package" => "prepay_id=" . $unifiedOrder->prepay_id,
            "signType" => 'MD5',
            "code_url" => $codeUrl[0],
        );
        $arr['paySign'] = self::getSign($arr, $config['key']);
        return $arr;
    }
    public function notify()
    {
        $config = array(
            'mch_id' => $this->mchid,
            'appid' => $this->appid,
            'key' => $this->apiKey,
        );
        $postStr = $GLOBALS["HTTP_RAW_POST_DATA"];
        $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
        if ($postObj === false) {
            die('parse xml error');
        }
        if ($postObj->return_code != 'SUCCESS') {
            die($postObj->return_msg);
        }
        if ($postObj->result_code != 'SUCCESS') {
            die($postObj->err_code);
        }
        $arr = (array)$postObj;
        unset($arr['sign']);
        if (self::getSign($arr, $config['key']) == $postObj->sign) {
            echo '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
            return $postObj;
        }
    }
    /**
     * curl get
     *
     * @param string $url
     * @param array $options
     * @return mixed
     */
    public static function curlGet($url = '', $options = array())
    {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        if (!empty($options)) {
            curl_setopt_array($ch, $options);
        }
        //https请求 不验证证书和host
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        $data = curl_exec($ch);
        curl_close($ch);
        return $data;
    }
    public static function curlPost($url = '', $postData = '', $options = array())
    {
        if (is_array($postData)) {
            $postData = http_build_query($postData);
        }
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30); //设置cURL允许执行的最长秒数
        if (!empty($options)) {
            curl_setopt_array($ch, $options);
        }
        //https请求 不验证证书和host
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        $data = curl_exec($ch);
        curl_close($ch);
        return $data;
    }
    public static function createNonceStr($length = 16)
    {
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $str = '';
        for ($i = 0; $i < $length; $i++) {
            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }
        return $str;
    }
    public static function arrayToXml($arr)
    {
        $xml = "<xml>";
        foreach ($arr as $key => $val) {
            if (is_numeric($val)) {
                $xml .= "<" . $key . ">" . $val . "</" . $key . ">";
            } else
                $xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">";
        }
        $xml .= "</xml>";
        return $xml;
    }
    /**
     * 获取签名
     */
    public static function getSign($params, $key)
    {
        ksort($params, SORT_STRING);
        $unSignParaString = self::formatQueryParaMap($params, false);
        $signStr = strtoupper(md5($unSignParaString . "&key=" . $key));
        return $signStr;
    }
    protected static function formatQueryParaMap($paraMap, $urlEncode = false)
    {
        $buff = "";
        ksort($paraMap);
        foreach ($paraMap as $k => $v) {
            if (null != $v && "null" != $v) {
                if ($urlEncode) {
                    $v = urlencode($v);
                }
                $buff .= $k . "=" . $v . "&";
            }
        }
        $reqPar = '';
        if (strlen($buff) > 0) {
            $reqPar = substr($buff, 0, strlen($buff) - 1);
        }
        return $reqPar;
    }
}